/*
 * GEmilErpelRRMultiRoundMoveMaker.java
 * Created on 4 déc. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.seasoncycle.movemaking.impl;


import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.standings.GStandings;
import com.hockeo.vhbase.data.helper.GMatchResult;
import com.hockeo.vhbase.data.helper.GMatchType;
import com.hockeo.vhbase.data.helper.GStandingsType;
import com.hockeo.vhbase.error.GException;

/**
 * Special move-maker for round robin contest elements where it is not necessary the best
 * strategy to completely empty the goal account because the same GA is then used in
 * following rounds.
 *
 * @version $Id$
 * @author jjanke
 */
public class GEmilErpelRRMultiRoundMoveMaker extends GEmilErpelRoundRobinMoveMaker
{
  // Calculated data
  //
  private Map<Integer, GStandings>      d_mapStandingsByRank;
  private final Map<GMatch, BigDecimal> d_mapAvailGoals = new HashMap<GMatch, BigDecimal>();

  @Override
  public void setup() throws GException
  {
    super.setup();

    // We need the standings by rank if this is the last moveday of the contest element,
    // but more rounds using the same GA are about to follow.
    //
    if ( getMovedayStatus() == MovedayStatus.LAST_OF_CONTEST_ELEMENT )
    {
      d_mapStandingsByRank = new TreeMap<Integer, GStandings>();

      List<GStandings> listAllStandings = getDao().findCurrentStandings( GStandingsType.TOTAL, d_elementRR );

      for ( GStandings s : listAllStandings )
        d_mapStandingsByRank.put( s.getRank(), s );
    }
  }

  /**
   * Determines whether the goals placing process should make such a move that it is
   * realistic to win the match. This method should not be called if only a few matches
   * are left (special handling of last moveday's matches provide a better algorithm for
   * determining how many goals should be placed).
   *
   * The algorithm implemented by this method does the following:
   *
   * 1) If it's a home match, return <code>true</code>.
   *
   * 2) If the points difference between both teams is inferior to 4 and less than 7
   * matches are left to be played, return <code>true</code>.
   *
   * 3) If the average number of goals difference necessary to win all outstanding home
   * and half of all outstanding away matches is greater than 1, return <code>true</code>.
   *
   * 4) Otherwise there is a chance of 1/3 depending on the number of outstanding away
   * matches that <code>true</code> is returned.
   *
   * @param match the match for which it should be determined whether it should be tried
   *          to win it or not
   * @return <code>true</code> if the match should be won if possible
   */
  @Override
  protected boolean wantsToWin( final GMatch match )
  {
    if ( d_resultRecommendation == MatchResultRecommendation.TRY_TO_WIN )
      return true;
    else if ( d_resultRecommendation == MatchResultRecommendation.MAY_LOSE )
      return false;

    // If the match is a home match, it should be tried to win it
    //
    if ( match.getMatchType( getClub() ) == GMatchType.HOME )
      return true;

    // In all other cases, we use a random generator by assuming that a club
    // wants to try to win 2/3 of his away matches (the probability to win will be 2/3)
    //
    final GStandings sAway = d_mapStandings.get( GStandingsType.AWAY );
    int[] anP = new int[2];
    anP[0] = sAway.getNumRemainingMatches();
    anP[1] = anP[0] + sAway.getNumRemainingMatches() * 2;

    final int nRandomNumber = new Random().nextInt( anP[1] );

    return nRandomNumber > anP[0];
  }

  /**
   * Returns the amount of goals that is still available for being placed. The amount of
   * goals that is considered to be required for winning matches after this round is
   * deducted from the returned amount.
   *
   * @see GEmilErpelRoundRobinMoveMaker#getNumAvailableGoals(GMatch, int)
   */
  @Override
  protected BigDecimal getNumAvailableGoals( final GMatch match, final int iMatch )
  {
    final int nMatchesLeft = getMatches().size() - iMatch;
    BigDecimal decGoals = d_mapAvailGoals.get( match );

    if ( decGoals == null )
    {
      // If we are on the last moveday and the club is already assured of participating in
      // the next round, we do not waste goals and return a number that is as low as
      // possible.
      //
      if ( getMovedayStatus() == MovedayStatus.LAST_OF_CONTEST_ELEMENT )
      {
        GStandings s = d_mapStandings.get( GStandingsType.TOTAL );

        switch ( s.getRankType() )
        {
          case PROMOTED_1:
            decGoals = BigDecimal.valueOf( getNumAvailableGoalsPromoted1( s, match, nMatchesLeft ) );
            break;

          case PROMOTED_2:
            decGoals = BigDecimal.valueOf( getNumAvailableGoalsPromoted2( s, match, nMatchesLeft ) );
            break;

          default:
            decGoals = BigDecimal.valueOf( getNumAvailableGoalsNonPromoted( s, match, nMatchesLeft ) );
            break;
        }
      }

      // If we still have no goals we use a random generator to calculate the number of
      // goals to deduct from the current amount of available goals
      //
      if ( decGoals == null )
      {
        int nGoalsRequiredAfterCurrentRound = (int) Math.ceil( getNumMatchesLeft()
            * GRulesConstants.GOALS_PER_MATCH_AVG );
        int[] anP = new int[nGoalsRequiredAfterCurrentRound + 1];
        manipulateProbabilitiesGauss( anP, nGoalsRequiredAfterCurrentRound, 2.0, 1.0 );

        int nRand = new Random().nextInt( anP[nGoalsRequiredAfterCurrentRound] );
        for ( int i = 0; i < anP.length; i++ )
          if ( nRand <= anP[i] )
          {
            decGoals = BigDecimal.valueOf( d_decAvailableGoals.doubleValue() - i );
            break;
          }
      }

      d_mapAvailGoals.put( match, decGoals );
    }

    return decGoals;
  }

  /**
   * Returns the number of goals that should be available if the club is currently having
   * a promoted 1 rank.
   *
   * @param s the current standings
   * @param match the match for which to return the available number of goals
   * @param nMatchesLeft the number of matches left to be played at this moveday
   * @return the available number of goals
   */
  private double getNumAvailableGoalsPromoted1( final GStandings s, final GMatch match, final int nMatchesLeft )
  {
    GStandings sFirstNonPromoted1 = d_mapStandingsByRank.get( d_elementRR.getNumPromoted1() + 1 );
    GStandings sFirstMidfield = d_mapStandingsByRank.get( d_elementRR.getNumPromoted1() + d_elementRR.getNumPromoted2()
        + 1 );

    // We use getMatches.size() as the number of left matches because we only have the
    // standings before the entire moveday and not the new ones after every match
    //
    int nPossiblePoints = getMatches().size() * GMatchResult.VICTORY.getNumPoints();

    // Are we sure to finish on a promoted 1 rank?
    //
    int nPointsDiff = s.getNumPoints() - sFirstNonPromoted1.getNumPoints();
    if ( nPointsDiff > nPossiblePoints )
      return 0;

    // If not, check if we are at least safe to be promoted (regardless of whether it is
    // promoted 1 or 2). If it's the case return 0 (even if we risk to finish 2nd it's
    // always a plus to take as many goals as possible into subsequent rounds).
    //
    if ( !sFirstNonPromoted1.equals( sFirstMidfield ) )
    {
      nPointsDiff = s.getNumPoints() - sFirstMidfield.getNumPoints();
      if ( nPointsDiff > nPossiblePoints )
        return 0;
    }

    return getNumDefaultAvailGoals( match, nMatchesLeft );
  }

  /**
   * Returns the number of goals that should be available if the club is currently having
   * a promoted 2 rank.
   *
   * @param s the current standings
   * @param match the match for which to return the available number of goals
   * @param nMatchesLeft the number of matches left to be played at this moveday
   * @return the available number of goals
   */
  private double getNumAvailableGoalsPromoted2( final GStandings s, final GMatch match, final int nMatchesLeft )
  {
    GStandings sFirstMidfield = d_mapStandingsByRank.get( d_elementRR.getNumPromoted1() + d_elementRR.getNumPromoted2()
        + 1 );

    // We use getMatches.size() as the number of left matches because we only have the
    // standings before the entire moveday and not the new ones after every match
    //
    int nPossiblePoints = getMatches().size() * GMatchResult.VICTORY.getNumPoints();

    // If we are safe, we leave it as it is and do not try to waste any more goals
    //
    int nPointsDiff = s.getNumPoints() - sFirstMidfield.getNumPoints();
    if ( nPointsDiff > nPossiblePoints )
      return 0;

    return getNumDefaultAvailGoals( match, nMatchesLeft );
  }

  /**
   * Returns the number of goals that should be available if the club is currently not
   * having a promoted 1/2 rank.
   *
   * @param s the current standings
   * @param match the match for which to return the available number of goals
   * @param nMatchesLeft the number of matches left to be played at this moveday
   * @return the available number of goals
   */
  private double getNumAvailableGoalsNonPromoted( final GStandings s, final GMatch match, final int nMatchesLeft )
  {
    GStandings sLastPromoted = d_mapStandingsByRank.get( d_elementRR.getNumPromoted1() + d_elementRR.getNumPromoted2() );

    // We use getMatches.size() as the number of left matches because we only have the
    // standings before the entire moveday and not the new ones after every match
    //
    int nPossiblePoints = getMatches().size() * GMatchResult.VICTORY.getNumPoints();

    int nPointsDiff = sLastPromoted.getNumPoints() - s.getNumPoints();

    // If the chances to clinch a promoted spot are rather low, than we are a bit more
    // generous with our remaining goals and return everything we have (there is nothing
    // to lose anymore)
    //
    if ( nPointsDiff > nPossiblePoints )
      return d_decAvailableGoals.doubleValue();

    return getNumDefaultAvailGoals( match, nMatchesLeft );
  }

  /**
   * Default algorithm for calculating the number of goals that should be available for
   * move-making. It should be used when the more specialised algorithms cannot be used.
   *
   * @param match the match for which to make the move
   * @param nMatchesLeft the number of matches left to be played at this moveday
   * @return the number of goals that are still available
   */
  private double getNumDefaultAvailGoals( final GMatch match, final int nMatchesLeft )
  {
    // Otherwise make sure to return at least as many goals to match the goals avg. of the
    // opponent
    //
    double dAvgGoals = d_decAvailableGoals.doubleValue() - ( getNumMatchesLeft() * GRulesConstants.GOALS_PER_MATCH_AVG );
    double dAvgGoalsOpp = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.TOTAL ).getRemainingGoals().doubleValue()
        / getRemainingMatchesToWinOpponent( match );

    // If exactly one match is left, we return an amount of goals that should be
    // sufficient to just have more goals than the adversary in average
    //
    if ( nMatchesLeft == 1 )
      return Math.ceil( ( dAvgGoalsOpp / match.getCostOfOneGoal( match.getOpponent( getClub() ) ).doubleValue() )
          * match.getCostOfOneGoal( getClub() ).doubleValue() );

    // Otherwise we take the minimum of our own and the opponents average and return this
    // value multuplied with the number of remaining matches
    //
    return Math.ceil( Math.min( dAvgGoalsOpp, dAvgGoals ) * nMatchesLeft );
  }

  /**
   * Returns the total number of outstanding matches. For short RR rounds followed by
   * other rounds, we consider all matches as required to be won. The subsequent matches
   * are not considered as the number of available goals is adjusted.
   *
   * @param match the match for which the move is currently calculated
   * @return the remaining number of matches to be won
   */
  @Override
  protected int getRemainingMatchesToWin( final GMatch match )
  {
    return d_nNumRemainingTotalMatches;
  }

  /**
   * Returns the total number of outstanding matches. For short RR rounds followed by
   * other rounds, we consider all matches as required to be won.
   *
   * @param match the match for which the move is currently calculated
   * @return the remaining number of matches to be won
   */
  @Override
  protected int getRemainingMatchesToWinOpponent( final GMatch match )
  {
    final GStandings sHome = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.HOME );
    final GStandings sAway = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.AWAY );

    return sHome.getNumRemainingMatches() + sAway.getNumRemainingMatches() + getNumMatchesLeft();
  }

  /**
   * Overridden to make sure that 0 goals are set if no goals should be wasted.
   */
  @Override
  protected int getAmountToSetWhenGoalsAccountIsEmpty( final GMatch match, final int iMatch )
  {
    BigDecimal decCost = match.getCostOfOneGoal( getClub() );

    // Only if we really have not enough goals left, we return something otherwise we want
    // to avoid placing goals for not good reason and return 0
    //
    if ( decCost.compareTo( d_decAvailableGoals ) > 0 )
      return 1;

    return 0;
  }
}
